home *** CD-ROM | disk | FTP | other *** search
/ PC World Komputer 2010 April / PCWorld0410.iso / pluginy Firefox / 138 / 138.xpi / chrome / stumbleupon.jar / content / DatabaseConnection.js < prev    next >
Text File  |  2009-05-22  |  26KB  |  1,122 lines

  1. /*
  2.     [IP:]
  3.     ***** BEGIN LICENSE BLOCK *****
  4.     
  5.     Copyright (c) 2006  Center for History and New Media
  6.                         George Mason University, Fairfax, Virginia, USA
  7.                         http://chnm.gmu.edu
  8.     
  9.     Licensed under the Educational Community License, Version 1.0 (the "License");
  10.     you may not use this file except in compliance with the License.
  11.     You may obtain a copy of the License at
  12.     
  13.     http://www.opensource.org/licenses/ecl1.php
  14.     
  15.     Unless required by applicable law or agreed to in writing, software
  16.     distributed under the License is distributed on an "AS IS" BASIS,
  17.     WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  18.     See the License for the specific language governing permissions and
  19.     limitations under the License.
  20.     
  21.     ***** END LICENSE BLOCK *****
  22.         
  23.         StumbleUpon revisions include:
  24.         1.  Changes namespace.
  25.         2.  Changes prototype syntax.
  26.         3.  Changes bracket convention.
  27.         4.  Adds missing semicolons.
  28.         5.  Replaces     Zotero.getZoteroDatabase() with new method
  29.             this.getDBFile().
  30.         6.  Replaces Zotero.getZoteroDirectory() with new method
  31.             this._getStorageDirectory().
  32.         7.  Replaces Zotero.moveToUnique() with new method
  33.             this._moveToUniqueFile().
  34.         8.  New debug logging that doesn't rely on the Zotero object.
  35.         9.  New user interaction that doesn't rely on the Zotero object.
  36.         10. Adds this._query and the a, as, av, als, alv, q, avr and v
  37.             methods.
  38.         11. Makes query() always return an array for SELECT queries.
  39.         12. Added _parent and automatic backup disabling.
  40.         13. Added _modified and made automatic backup dependent on
  41.             _modified.
  42.         14. Disabled prompt for restart upon detecting corruption.
  43.         15. Added _hostIsWindows().
  44. */
  45.  
  46. var su_DatabaseConnection = function(parent, dbName)
  47. {
  48.     this.skipBackup = false;
  49.     
  50.     // Private members
  51.     this._parent = parent;
  52.     this._dbName = dbName;
  53.     this._modified = false;
  54.     this._shutdown = false;
  55.     this._connection = null;
  56.     this._transactionRollback = null;
  57.     this._transactionNestingLevel = 0;
  58.     this._callbacks = { begin: [], commit: [], rollback: [] };
  59.     this._dbIsCorrupt = null;
  60.     this._self = this;
  61.     this._query = "";
  62. }
  63. su_DatabaseConnection.prototype =
  64. { // BEGIN prototype
  65.  
  66. /////////////////////////////////////////////////////////////////
  67. //
  68. // Public methods
  69. //
  70. /////////////////////////////////////////////////////////////////
  71.  
  72. /*
  73.  * Run an SQL query
  74.  *
  75.  *  Optional _params_ is an array of bind parameters in the form
  76.  *        [1,"hello",3] or [{'int':2},{'string':'foobar'}]
  77.  *
  78.  *     Returns:
  79.  *       - Associative array (similar to mysql_fetch_assoc) for SELECT's
  80.  *     - lastInsertId for INSERT's
  81.  *     - TRUE for other successful queries
  82.  *     - FALSE on error
  83.  */
  84. query: function (sql,params)
  85. {
  86.     var db = this._getDBConnection();
  87.     if (! sql)
  88.         sql = this._query;
  89.     
  90.     try {
  91.         // Parse out the SQL command being used
  92.         var op = sql.match(/^[^a-z]*[^ ]+/i);
  93.         if (op)
  94.             op = op.toString().toLowerCase();
  95.         
  96.         // If SELECT statement, return result
  97.         if (op=='select')
  98.         {
  99.             // Until the native dataset methods work (or at least exist),
  100.             // we build a multi-dimensional associative array manually
  101.             
  102.             var statement = this.getStatement(sql, params);
  103.             
  104.             var dataset = new Array();
  105.             while (statement.executeStep())
  106.             {
  107.                 var row = new Array();
  108.                 
  109.                 for(var i=0, len=statement.columnCount; i<len; i++)
  110.                     row[statement.getColumnName(i)] = this._getTypedValue(statement, i);
  111.                 dataset.push(row);
  112.             }
  113.             statement.reset();
  114.             
  115.             return dataset;
  116.         }
  117.         else
  118.         {
  119.             this._modified = true;
  120.             if (params)
  121.             {
  122.                 var statement = this.getStatement(sql, params);
  123.                 statement.execute();
  124.             }
  125.             else
  126.             {
  127.                 this._debug(sql,5);
  128.                 db.executeSimpleSQL(sql);
  129.             }
  130.             
  131.             if (op=='insert')
  132.                 return db.lastInsertRowID;
  133.             // DEBUG: Can't get affected rows for UPDATE or DELETE?
  134.             else
  135.                 return true;
  136.         }
  137.     }
  138.     catch (e) {
  139.         this.checkException(e);
  140.         
  141.         var dberr = (db.lastErrorString!='not an error')
  142.             ? ' [ERROR: ' + db.lastErrorString + ']' : '';
  143.         throw(e + ' [QUERY: ' + sql + ']' + dberr);
  144.     }
  145. },
  146.  
  147.  
  148. /*
  149.  * Query a single value and return it
  150.  */
  151. valueQuery: function (sql,params)
  152. {
  153.     var statement = this.getStatement(sql, params);
  154.     
  155.     // No rows
  156.     if (!statement.executeStep())
  157.     {
  158.         statement.reset();
  159.         return false;
  160.     }
  161.     
  162.     var value = this._getTypedValue(statement, 0);
  163.     statement.reset();
  164.     return value;
  165. },
  166.  
  167.  
  168. /*
  169.  * Run a query and return the first row
  170.  */
  171. rowQuery: function (sql,params)
  172. {
  173.     var result = this.query(sql,params);
  174.     if (result.length)
  175.         return result[0];
  176.     else
  177.         return new Array();
  178. },
  179.  
  180.  
  181. /*
  182.  * Run a query and return the first column as a numerically-indexed array
  183.  */
  184. columnQuery: function (sql,params)
  185. {
  186.     var statement = this.getStatement(sql, params);
  187.     
  188.     if (statement)
  189.     {
  190.         var column = new Array();
  191.         while (statement.executeStep())
  192.             column.push(this._getTypedValue(statement, 0));
  193.         
  194.         statement.reset();
  195.         return column.length ? column : false;
  196.     }
  197.     return false;
  198. },
  199.  
  200. a: function (str)
  201. {
  202.     this._query = str;
  203. },
  204.  
  205. av: function (str)
  206. {
  207.     str = ((parseInt(str)) + "");
  208.     if (str == "NaN")
  209.         str = "0";
  210.     this._query += str + ",";    
  211. },
  212.  
  213. avr: function (str)
  214. {
  215.     str = ((parseInt(str)) + "");
  216.     if (str == "NaN")
  217.         str = "0";
  218.     this._query += str;
  219. },
  220.  
  221. as: function (str)
  222. {
  223.     this._query += "'" + str.replace(/'/g, "''") + "',";
  224. },
  225.  
  226. alv: function (str)
  227. {
  228.     str = ((parseInt(str)) + "");
  229.     if (str == "NaN")
  230.         str = "0";
  231.     this._query += str + ")";    
  232. },
  233.  
  234. als: function (str)
  235. {
  236.     this._query += "'" + str.replace(/'/g, "''") + "')";
  237. },
  238.  
  239. escapeString: function (str)
  240. {
  241.     this._query += str.replace(/'/g, "''");
  242. },
  243.  
  244. q: function (str)
  245. {
  246.     return "'" + str.replace(/'/g, "''") + "'";
  247. },
  248.  
  249. v: function (str)
  250. {
  251.     str = ((parseInt(str)) + "");
  252.     if (str == "NaN")
  253.         str = "0";
  254.     return str;
  255. },
  256.  
  257. /*
  258. /*
  259.  * Get a raw mozStorage statement from the DB for manual processing
  260.  *
  261.  * This should only be used externally for manual parameter binding for
  262.  * large repeated queries
  263.  *
  264.  *  Optional _params_ is an array of bind parameters in the form
  265.  *        [1,"hello",3] or [{'int':2},{'string':'foobar'}]
  266.  */
  267. getStatement: function (sql, params)
  268. {
  269.     var db = this._getDBConnection();
  270.     
  271.     try {
  272.         this._debug(sql,5);
  273.         var statement = db.createStatement(sql);
  274.     }
  275.     catch (e) {
  276.         var dberr = (db.lastErrorString!='not an error')
  277.             ? ' [ERROR: ' + db.lastErrorString + ']' : '';
  278.         throw(e + ' [QUERY: ' + sql + ']' + dberr);
  279.     }
  280.     
  281.     if (params)
  282.     {
  283.         // If single scalar value or single non-array object, wrap in an array
  284.         if (typeof params != 'object' || params===null ||
  285.                 (params && typeof params == 'object' && !params.length))
  286.             params = [params];
  287.         
  288.         for (var i=0; i<params.length; i++)
  289.         {
  290.             // Integer
  291.             if (params[i]!==null && typeof params[i]['int'] != 'undefined')
  292.             {
  293.                 var type = 'int';
  294.                 var value = params[i]['int'];
  295.             }
  296.             // String
  297.             else if (params[i]!==null && typeof params[i]['string'] != 'undefined')
  298.             {
  299.                 var type = 'string';
  300.                 var value = params[i]['string'];
  301.             }
  302.             // Null
  303.             else if (params[i]!==null && typeof params[i]['null'] != 'undefined')
  304.             {
  305.                 var type = 'null';
  306.             }
  307.             // Automatic (trust the JS type)
  308.             else
  309.             {
  310.                 switch (typeof params[i])
  311.                 {
  312.                     case 'string':
  313.                         var type = 'string';
  314.                         break;
  315.                     case 'number':
  316.                         var type = 'int';
  317.                         break;
  318.                     // Object
  319.                     default:
  320.                         if (params[i]===null)
  321.                         {
  322.                             var type = 'null';
  323.                         }
  324.                         else
  325.                         {
  326.                             throw('Invalid bound parameter ' + params[i]);
  327. //                            throw('Invalid bound parameter ' + params[i] +
  328. //                                ' in ' + Zotero.varDump(params));
  329.                         }
  330.                 }
  331.                 var value = params[i];
  332.             }
  333.             
  334.             // Bind the parameter as the correct type
  335.             switch (type)
  336.             {
  337.                 case 'int':
  338.                     this._debug('Binding parameter ' + (i+1)
  339.                         + ' of type int: ' + value, 5);
  340.                     statement.bindInt32Parameter(i, value);
  341.                     break;
  342.                     
  343.                 case 'string':
  344.                     this._debug('Binding parameter ' + (i+1)
  345.                         + ' of type string: "' + value + '"', 5);
  346.                     statement.bindUTF8StringParameter(i, value);
  347.                     break;
  348.                     
  349.                 case 'null':
  350.                     this._debug('Binding parameter ' + (i+1)
  351.                         + ' of type NULL', 5);
  352.                     statement.bindNullParameter(i);
  353.                     break;
  354.             }
  355.         }
  356.     }
  357.     return statement;
  358. },
  359.  
  360.  
  361. /*
  362.  * Only for use externally with this.getStatement()
  363.  */
  364. getLastInsertID: function ()
  365. {
  366.     var db = this._getDBConnection();
  367.     return db.lastInsertRowID;
  368. },
  369.  
  370.  
  371. /*
  372.  * Only for use externally with this.getStatement()
  373.  */
  374. getLastErrorString: function ()
  375. {
  376.     var db = this._getDBConnection();
  377.     return db.lastErrorString;
  378. },
  379.  
  380.  
  381. beginTransaction: function ()
  382. {
  383.     var db = this._getDBConnection();
  384.     
  385.     if (db.transactionInProgress)
  386.     {
  387.         this._transactionNestingLevel++;
  388.         this._debug('Transaction in progress -- increasing level to '
  389.             + this._transactionNestingLevel, 5);
  390.     }
  391.     else
  392.     {
  393.         this._debug('Beginning DB transaction', 5);
  394.         db.beginTransaction();
  395.         
  396.         // Run callbacks
  397.         for (var i=0; i<this._callbacks.begin.length; i++)
  398.         {
  399.             if (this._callbacks.begin[i])
  400.                 this._callbacks.begin[i]();
  401.         }
  402.     }
  403. },
  404.  
  405.  
  406. commitTransaction: function ()
  407. {
  408.     var db = this._getDBConnection();
  409.     
  410.     if (this._transactionNestingLevel)
  411.     {
  412.         this._transactionNestingLevel--;
  413.         this._debug('Decreasing transaction level to ' + this._transactionNestingLevel, 5);
  414.     }
  415.     else if (this._transactionRollback)
  416.     {
  417.         this._debug('Rolling back previously flagged transaction', 5);
  418.         this.rollbackTransaction();
  419.     }
  420.     else
  421.     {
  422.         this._debug('Committing transaction',5);
  423.         try {
  424.             db.commitTransaction();
  425.             
  426.             // Run callbacks
  427.             for (var i=0; i<this._callbacks.commit.length; i++)
  428.             {
  429.                 if (this._callbacks.commit[i])
  430.                     this._callbacks.commit[i]();
  431.             }
  432.         }
  433.         catch(e) {
  434.             var dberr = (db.lastErrorString!='not an error')
  435.                 ? ' [ERROR: ' + db.lastErrorString + ']' : '';
  436.             throw(e + dberr);
  437.         }
  438.     }
  439. },
  440.  
  441.  
  442. rollbackTransaction: function ()
  443. {
  444.     var db = this._getDBConnection();
  445.     
  446.     if (!db.transactionInProgress)
  447.     {
  448.         this._debug("Transaction is not in progress in rollbackTransaction()", 2);
  449.         return;
  450.     }
  451.     
  452.     if (this._transactionNestingLevel)
  453.     {
  454.         this._transactionNestingLevel--;
  455.         this._transactionRollback = true;
  456.         this._debug('Flagging nested transaction for rollback', 5);
  457.     }
  458.     else
  459.     {
  460.         this._debug('Rolling back transaction', 5);
  461.         this._transactionRollback = false;
  462.         try {
  463.             db.rollbackTransaction();
  464.             
  465.             // Run callbacks
  466.             for (var i=0; i<this._callbacks.rollback.length; i++)
  467.             {
  468.                 if (this._callbacks.rollback[i])
  469.                     this._callbacks.rollback[i]();
  470.             }
  471.         }
  472.         catch(e) {
  473.             var dberr = (db.lastErrorString!='not an error')
  474.                 ? ' [ERROR: ' + db.lastErrorString + ']' : '';
  475.             throw(e + dberr);
  476.         }
  477.     }
  478. },
  479.  
  480.  
  481. addCallback: function (type, cb)
  482. {
  483.     switch (type)
  484.     {
  485.         case 'begin':
  486.         case 'commit':
  487.         case 'rollback':
  488.             break;
  489.             
  490.         default:
  491.             throw ("Invalid callback type '" + type + "' in DB.addCallback()");
  492.     }
  493.     
  494.     var id = this._callbacks[type].length;
  495.     this._callbacks[type][id] = cb;
  496.     return id;
  497. },
  498.  
  499.  
  500. removeCallback: function (type, id)
  501. {
  502.     switch (type)
  503.     {
  504.         case 'begin':
  505.         case 'commit':
  506.         case 'rollback':
  507.             break;
  508.             
  509.         default:
  510.             throw ("Invalid callback type '" + type + "' in DB.removeCallback()");
  511.     }
  512.     
  513.     delete this._callbacks[type][id];
  514. },
  515.  
  516.  
  517. transactionInProgress: function ()
  518. {
  519.     var db = this._getDBConnection();
  520.     return db.transactionInProgress;
  521. },
  522.  
  523.  
  524. /**
  525.  * Safety function used on shutdown to make sure we're not stuck in the
  526.  * middle of a transaction
  527.  *
  528.  * NOTE: No longer used
  529.  */
  530. commitAllTransactions: function ()
  531. {
  532.     if (this.transactionInProgress())
  533.     {
  534.         var level = this._transactionNestingLevel;
  535.         this._transactionNestingLevel = 0;
  536.         try {
  537.             this.commitTransaction();
  538.         }
  539.         catch (e) {}
  540.         return level ? level : true;
  541.     }
  542.     return false;
  543. },
  544.  
  545.  
  546. /*
  547.  * Used on shutdown to rollback all open transactions
  548.  */
  549. rollbackAllTransactions: function ()
  550. {
  551.     if (this.transactionInProgress())
  552.     {
  553.         var level = this._transactionNestingLevel;
  554.         this._transactionNestingLevel = 0;
  555.         try {
  556.             this.rollbackTransaction();
  557.         }
  558.         catch (e) {}
  559.         return level ? level : true;
  560.     }
  561.     return false;
  562. },
  563.  
  564.  
  565. tableExists: function (table)
  566. {
  567.     return this._getDBConnection().tableExists(table);
  568. },
  569.  
  570.  
  571. getColumns: function (table)
  572. {
  573.     var db = this._getDBConnection();
  574.     
  575.     try {
  576.         var sql = "SELECT * FROM " + table + " LIMIT 1";
  577.         var statement = this.getStatement(sql);
  578.         var cols = new Array();
  579.         for (var i=0,len=statement.columnCount; i<len; i++)
  580.             cols.push(statement.getColumnName(i));
  581.         statement.reset();
  582.         return cols;
  583.     }
  584.     catch (e) {
  585.         this._debug(e,1);
  586.         return false;
  587.     }
  588. },
  589.  
  590.  
  591. getColumnHash: function (table)
  592. {
  593.     var cols = this.getColumns(table);
  594.     var hash = {};
  595.     if (cols.length)
  596.     {
  597.         for (var i=0; i<cols.length; i++)
  598.             hash[cols[i]] = true;
  599.     }
  600.     return hash;
  601. },
  602.  
  603.  
  604. /**
  605. * Find the lowest unused integer >0 in a table column
  606. *
  607. * Note: This retrieves all the rows of the column, so it's not really
  608. *    meant for particularly large tables.
  609. **/
  610. getNextID: function (table, column)
  611. {
  612.     var sql = 'SELECT ' + column + ' FROM ' + table + ' ORDER BY ' + column;
  613.     var vals = this.columnQuery(sql);
  614.     
  615.     if (!vals)
  616.         return 1;
  617.     
  618.     if (vals[0] === '0')
  619.         vals.shift();
  620.     
  621.     for (var i=0, len=vals.length; i<len; i++)
  622.     {
  623.         if (vals[i] != i+1)
  624.             break;
  625.     }
  626.     
  627.     return i+1;
  628. },
  629.  
  630.  
  631. /**
  632. * Find the next lowest numeric suffix for a value in table column
  633. *
  634. * For example, if "Untitled" and "Untitled 2" and "Untitled 4",
  635. * returns "Untitled 3"
  636. *
  637. * DEBUG: doesn't work once there's an "Untitled 10"
  638. *
  639. * If _name_ alone is available, returns that
  640. **/
  641. getNextName: function (table, field, name)
  642. {
  643.     var sql = "SELECT " + field + " FROM " + table + " WHERE " + field
  644.         + " LIKE ? ORDER BY " + field + " COLLATE NOCASE";
  645.     var untitleds = this.columnQuery(sql, name + '%');
  646.     
  647.     if (!untitleds || untitleds[0]!=name)
  648.         return name;
  649.     
  650.     var i = 1;
  651.     var num = 2;
  652.     while (untitleds[i] && untitleds[i]==(name + ' ' + num))
  653.     {
  654.         while (untitleds[i+1] && untitleds[i]==untitleds[i+1])
  655.         {
  656.             this._debug('Next ' + i + ' is ' + untitleds[i]);
  657.             i++;
  658.         }
  659.         
  660.         i++;
  661.         num++;
  662.     }
  663.     
  664.     return name + ' ' + num;
  665. },
  666.  
  667.  
  668. /*
  669.  * Shutdown observer -- implements nsIObserver
  670.  */
  671. observe: function(subject, topic, data)
  672. {
  673.     switch (topic)
  674.     {
  675.         case 'xpcom-shutdown':
  676.             this.destroy();
  677.             
  678.             break;
  679.     }
  680. },
  681.  
  682. destroy: function ()
  683. {
  684.     if (this._shutdown)
  685.     {
  686.         this._debug('returning');
  687.         return;
  688.     }
  689.     
  690.     // NOTE: disabled
  691.     //var level = this.commitAllTransactions();
  692.     var level = this.rollbackAllTransactions();
  693.     if (level)
  694.     {
  695.         level = level === true ? '0' : level;
  696.         this._debug("A transaction in DB '" + this._dbName + "' was still open! (level " + level + ")", 2);
  697.     }
  698.     
  699.     this._shutdown = true;
  700.     
  701.     this.skipBackup = (this.skipBackup || (! this._modified) || (! this._parent.getValue("@enable_db_backup"))); 
  702.     
  703.     this.backupDatabase();
  704.     
  705.     this._parent = null;
  706. },
  707.  
  708. integrityCheck: function ()
  709. {
  710.     var ok = this.valueQuery("PRAGMA integrity_check");
  711.     return ok == 'ok';
  712. },
  713.  
  714.  
  715. checkException: function (e)
  716. {
  717.     if (e.name && e.name == 'NS_ERROR_FILE_CORRUPTED')
  718.     {
  719.         // Write corrupt marker to data directory
  720.         var file = this.getDBFile(this._dbName, 'is.corrupt');
  721.         var foStream = Components.classes["@mozilla.org/network/file-output-stream;1"]
  722.                          .createInstance(Components.interfaces.nsIFileOutputStream);
  723.         foStream.init(file, 0x02 | 0x08 | 0x20, 0664, 0); // write, create, truncate
  724.         foStream.write('', 0);
  725.         foStream.close();
  726.         
  727.         this._dbIsCorrupt = true;
  728.         
  729. //        var ps = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
  730. //                                .getService(Components.interfaces.nsIPromptService);
  731. //        
  732. //        var buttonFlags = (ps.BUTTON_POS_0) * (ps.BUTTON_TITLE_IS_STRING)
  733. //            + (ps.BUTTON_POS_1) * (ps.BUTTON_TITLE_IS_STRING);
  734. //        
  735. //        var index = ps.confirmEx(null,
  736. //            Zotero.getString('general.error'),
  737. //            Zotero.getString('db.dbCorrupted', this._dbName) + '\n\n' + Zotero.getString('db.dbCorrupted.restart'),
  738. //            buttonFlags,
  739. //            Zotero.getString('general.restartNow'),
  740. //            Zotero.getString('general.restartLater'),
  741. //            null, null, {});
  742. //        
  743. //        if (index == 0)
  744. //        {
  745. //            var appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"]
  746. //                    .getService(Components.interfaces.nsIAppStartup);
  747. //            appStartup.quit(Components.interfaces.nsIAppStartup.eRestart);
  748. //            appStartup.quit(Components.interfaces.nsIAppStartup.eAttemptQuit);
  749. //        }
  750. //        
  751. //        Zotero.skipLoading = true;
  752. //        return false;
  753.     }
  754.     return true;
  755. },
  756.  
  757.  
  758. backupDatabase: function (suffix)
  759. {
  760.     if (this.transactionInProgress())
  761.     {
  762.         this._debug("Transaction in progress--skipping backup of DB '" + this._dbName + "'", 2);
  763.         return false;
  764.     }
  765.     
  766.     var corruptMarker = this.getDBFile(this._dbName, 'is.corrupt').exists();
  767.     
  768.     if (this.skipBackup)
  769.     {
  770.         this._debug("Skipping backup of database '" + this._dbName + "'", 1);
  771.         return false;
  772.     }
  773.     else if (this._dbIsCorrupt || corruptMarker)
  774.     {
  775.         this._debug("Database '" + this._dbName + "' is marked as corrupt--skipping backup", 1);
  776.         return false;
  777.     }
  778.     
  779.     this._debug("Backing up database '" + this._dbName + "'");
  780.     
  781.     var file = this.getDBFile(this._dbName);
  782.     var backupFile = this.getDBFile(this._dbName,
  783.         (suffix ? suffix + '.' : '') + 'bak');
  784.     
  785.     // Copy via a temporary file so we don't run into disk space issues
  786.     // after deleting the old backup file
  787.     var tmpFile = this.getDBFile(this._dbName, 'tmp');
  788.     if (tmpFile.exists())
  789.         tmpFile.remove(null);
  790.     
  791.     try {
  792.         file.copyTo(file.parent, tmpFile.leafName);
  793.     }
  794.     catch (e){
  795.         // TODO: deal with low disk space
  796.         throw (e);
  797.     }
  798.     
  799.     // Opened database files can't be moved on Windows, so we have to skip
  800.     // the extra integrity check (unless we wanted to write two copies of
  801.     // the database, but that doesn't seem like a great idea)
  802.     if (!this._hostIsWindows())
  803.     {
  804.         try {
  805.             var store = Components.classes["@mozilla.org/storage/service;1"].
  806.                 getService(Components.interfaces.mozIStorageService);
  807.                 
  808.             var connection = store.openDatabase(tmpFile);
  809.         }
  810.         catch (e){
  811.             this._debug("Database file '" + tmpFile.leafName + "' is corrupt--skipping backup");
  812.             if (tmpFile.exists())
  813.                 tmpFile.remove(null);
  814.             return false;
  815.         }
  816.     }
  817.     
  818.     // Remove old backup file
  819.     if (backupFile.exists())
  820.         backupFile.remove(null);
  821.     
  822.     tmpFile.moveTo(tmpFile.parent, backupFile.leafName);
  823.     
  824.     return true;
  825. },
  826.  
  827. _hostIsWindows: function ()
  828. {
  829.     var outval = true;
  830.     try {
  831.         var appinfo = Components.classes["@mozilla.org/xre/app-info;1"]
  832.                 .getService(Components.interfaces.nsIXULAppInfo);
  833.         appinfo = appinfo.QueryInterface(Components.interfaces.nsIXULRuntime);
  834.  
  835. //        spec.id = appinfo.ID;
  836. //        spec.version = appinfo.version;
  837.         outval = (appinfo.OS.toLowerCase().indexOf("win") != -1);
  838.     } catch (e) {}
  839.     
  840.     return outval;
  841. },
  842.  
  843. /*
  844.  * Keep the SQLite shared cache live between transactions with a dummy statement,
  845.  * which speeds up DB access dramatically (at least on Windows and Linux--OS X
  846.  * seems to be much faster already, perhaps due to its own disk cache)
  847.  *
  848.  * This is the same technique used by Mozilla code. The one downside is that it
  849.  * prevents schema changes, so this is called after schema updating. If the
  850.  * schema really needs to be updated at another point, use stopDummyStatement().
  851.  *
  852.  * See http://developer.mozilla.org/en/docs/Storage:Performance for more info.
  853.  */
  854. startDummyStatement: function ()
  855. {
  856. //    try {
  857.         if (!this._dummyConnection)
  858.         {
  859.             this._debug("Opening database '" + this._dbName + " for dummy statement");
  860.             // Get the storage service
  861.             var store = Components.classes["@mozilla.org/storage/service;1"].
  862.                 getService(Components.interfaces.mozIStorageService);
  863.             var file = this.getDBFile(this._dbName);
  864.             this._dummyConnection = store.openDatabase(file);
  865.         }
  866.         
  867.         if (this._dummyStatement)
  868.         {
  869.             this._debug("Dummy statement is already open");
  870.             return;
  871.         }
  872.         
  873.         this._debug("Initializing dummy statement for '" + this._dbName + "'");
  874.         
  875.         var sql = "CREATE TABLE IF NOT EXISTS dummyTable (id INTEGER PRIMARY KEY)";
  876.         this._dummyConnection.executeSimpleSQL(sql);
  877.         
  878.         sql = "INSERT OR IGNORE INTO dummyTable VALUES (1)";
  879.         this._dummyConnection.executeSimpleSQL(sql);
  880.         
  881.         sql = "SELECT id FROM dummyTable LIMIT 1";
  882.         this._dummyStatement = this._dummyConnection.createStatement(sql);
  883.         this._dummyStatement.executeStep();
  884.     
  885. //    }
  886. //    catch (e) {
  887. //        Components.utils.reportError(e);
  888. //        this._debug(e);
  889. //    }
  890. },
  891.  
  892.  
  893. /*
  894.  * Stop the dummy statement temporarily to allow for schema changess
  895.  *
  896.  * The statement needs to be started again or performance will suffer.
  897.  */
  898. stopDummyStatement: function ()
  899. {
  900.     if (!this._dummyStatement)
  901.         return;
  902.     
  903.     this._debug("Stopping dummy statement for '" + this._dbName + "'");
  904.     this._dummyStatement.reset();
  905.     this._dummyStatement = null;
  906. },
  907.  
  908.  
  909. getDBFile: function (name, ext)
  910. {
  911.     var str;
  912.     
  913.     if (name)
  914.         str = name;
  915.     else if (this._dbName)
  916.         str = this._dbName;
  917.     else
  918.         str = 'stumbleupon';
  919.     
  920.     str += '.sqlite';
  921.     
  922.     str += (ext) ? ('.' + ext) : '';
  923.     
  924.     var file = this._getStorageDirectory();
  925.     
  926.     file.append(str);
  927.     
  928.     return file;
  929. },
  930.  
  931.  
  932. /////////////////////////////////////////////////////////////////
  933. //
  934. // Private methods
  935. //
  936. /////////////////////////////////////////////////////////////////
  937.  
  938. _getStorageDirectory: function ()
  939. {
  940.     var file = Components.classes["@mozilla.org/file/directory_service;1"]
  941.                 .getService(Components.interfaces.nsIProperties)
  942.                 .get("ProfD", Components.interfaces.nsIFile);
  943.  
  944.     file.append("StumbleUpon");
  945.     if (! file.exists())
  946.         file.create(file.DIRECTORY_TYPE, 0700);
  947.     
  948.     return file;    
  949. },
  950.  
  951. _moveToUniqueFile: function (file, newFile)
  952. {
  953.     newFile.createUnique(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0644);
  954.     var newName = newFile.leafName;
  955.     newFile.remove(null);
  956.     
  957.     // Move file to unique name
  958.     file.moveTo(newFile.parent, newName);
  959.     return file;
  960. },
  961.  
  962. /*
  963.  * Retrieve a link to the data store
  964.  */
  965. _getDBConnection: function ()
  966. {
  967.     if (this._connection)
  968.         return this._connection;
  969.     
  970.     this._debug("Opening database '" + this._dbName + "'");
  971.     
  972.     // Get the storage service
  973.     var store = Components.classes["@mozilla.org/storage/service;1"].
  974.         getService(Components.interfaces.mozIStorageService);
  975.     
  976.     var file = this.getDBFile(this._dbName);
  977.     var backupFile = this.getDBFile(this._dbName, 'bak');
  978.     
  979.     var fileName = this._dbName + '.sqlite';
  980.     
  981. //    if (this._dbName == 'zotero' && ZOTERO_CONFIG['DB_REBUILD'])
  982. //    {
  983. //        if (confirm('Erase all user data and recreate database from schema?'))
  984. //        {
  985. //            // Delete existing Zotero database
  986. //            if (file.exists())
  987. //                file.remove(null);
  988. //            
  989. //            // Delete existing storage folder
  990. //            var dir = Zotero.getStorageDirectory();
  991. //            if (dir.exists())
  992. //                dir.remove(true);
  993. //        }
  994. //    }
  995.     
  996.     catchBlock: try {
  997.         var corruptMarker = this.getDBFile(this._dbName, 'is.corrupt');
  998.         if (corruptMarker.exists())
  999.             throw({ name: 'NS_ERROR_FILE_CORRUPTED' })
  1000.         this._connection = store.openDatabase(file);
  1001.     }
  1002.     catch (e) {
  1003.         if (e.name=='NS_ERROR_FILE_CORRUPTED')
  1004.         {
  1005.             this._debug("Database file '" + file.leafName + "' corrupted", 1);
  1006.             
  1007.             // No backup file! Eek!
  1008.             if (!backupFile.exists())
  1009.             {
  1010.                 this._debug("No backup file for DB '" + this._dbName + "' exists", 1);
  1011.                 
  1012.                 // Save damaged filed
  1013.                 this._debug('Saving damaged DB file with .damaged extension', 1);
  1014.                 var damagedFile = this.getDBFile(this._dbName, 'damaged');
  1015.                 this._moveToUniqueFile(file, damagedFile);
  1016.                 
  1017.                 // Create new main database
  1018.                 var file = this.getDBFile(this._dbName);
  1019.                 this._connection = store.openDatabase(file);
  1020.                 
  1021.                 if (corruptMarker.exists())
  1022.                     corruptMarker.remove(null);
  1023.                 
  1024. //                alert(Zotero.getString('db.dbCorruptedNoBackup', fileName));
  1025.                 break catchBlock;
  1026.             }
  1027.             
  1028.             // Save damaged file
  1029.             this._debug('Saving damaged DB file with .damaged extension', 1);
  1030.             var damagedFile = this.getDBFile(this._dbName, 'damaged');
  1031.             this._moveToUniqueFile(file, damagedFile);
  1032.             
  1033.             // Test the backup file
  1034.             try {
  1035.                 this._connection = store.openDatabase(backupFile);
  1036.             }
  1037.             // Can't open backup either
  1038.             catch (e) {
  1039.                 // Create new main database
  1040.                 var file = this.getDBFile(this._dbName);
  1041.                 this._connection = store.openDatabase(file);
  1042.                 
  1043. //                alert(Zotero.getString('db.dbRestoreFailed', fileName));
  1044.                 
  1045.                 if (corruptMarker.exists())
  1046.                     corruptMarker.remove(null);
  1047.                 
  1048.                 break catchBlock;
  1049.             }
  1050.             
  1051.             this._connection = undefined;
  1052.             
  1053.             // Copy backup file to main DB file
  1054.             this._debug("Restoring database '" + this._dbName + "' from backup file", 1);
  1055.             try {
  1056.                 backupFile.copyTo(backupFile.parent, fileName);
  1057.             }
  1058.             catch (e) {
  1059.                 // TODO: deal with low disk space
  1060.                 throw (e);
  1061.             }
  1062.             
  1063.             // Open restored database
  1064.             var file = this._getStorageDirectory();
  1065.             file.append(fileName);
  1066.             this._connection = store.openDatabase(file);
  1067.             this._debug('Database restored', 1);
  1068. //            var msg = Zotero.getString('db.dbRestored', [
  1069. //                fileName,
  1070. //                Zotero.Date.getFileDateString(backupFile),
  1071. //                Zotero.Date.getFileTimeString(backupFile)
  1072. //            ]);
  1073. //            alert(msg);
  1074.             
  1075.             if (corruptMarker.exists())
  1076.                 corruptMarker.remove(null);
  1077.             
  1078.             break catchBlock;
  1079.         }
  1080.         
  1081.         // Some other error that we don't yet know how to deal with
  1082.         throw (e);
  1083.     }
  1084.     
  1085.     // Register shutdown handler to call this.onShutdown() for DB backup
  1086.     var observerService = Components.classes["@mozilla.org/observer-service;1"]
  1087.         .getService(Components.interfaces.nsIObserverService);
  1088.     observerService.addObserver(this, "xpcom-shutdown", false);
  1089.     observerService = null;
  1090.     
  1091.     return this._connection;
  1092. },
  1093.  
  1094.  
  1095. _debug: function (str, level)
  1096. {
  1097.     //this._parent._logError(false, {}, "DB ", str);
  1098. },
  1099.  
  1100.  
  1101. _getTypedValue: function (statement, i)
  1102. {
  1103.     var type = statement.getTypeOfIndex(i);
  1104.     switch (type)
  1105.     {
  1106.         case statement.VALUE_TYPE_INTEGER:
  1107.             return statement.getInt64(i);
  1108.         case statement.VALUE_TYPE_TEXT:
  1109.             return statement.getUTF8String(i);
  1110.         case statement.VALUE_TYPE_NULL:
  1111.             return null;
  1112.         case statement.VALUE_TYPE_FLOAT:
  1113.             return statement.getDouble(i);
  1114.         case statement.VALUE_TYPE_BLOB:
  1115.             return statement.getBlob(i);
  1116.         default:
  1117.             return null;
  1118.     }
  1119. }
  1120.  
  1121. } // END prototype
  1122.